package com.amumu.drama.common.modules.aliyunoss.processor;

import com.alibaba.fastjson.JSON;
import com.aliyun.oss.ClientConfiguration;
import com.aliyun.oss.ClientException;
import com.aliyun.oss.OSSClient;
import com.aliyun.oss.OSSException;
import com.aliyun.oss.common.auth.DefaultCredentialProvider;
import com.aliyun.oss.common.comm.Protocol;
import com.aliyun.oss.common.utils.IOUtils;
import com.aliyun.oss.model.*;
import com.amumu.drama.common.modules.aliyunoss.config.AliyunOssConfig;
import com.amumu.drama.common.modules.aliyunoss.constant.OssBucket;
import com.amumu.drama.common.modules.aliyunoss.dto.OssCallback;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import com.google.common.hash.Hashing;
import com.google.common.io.BaseEncoding;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpEntity;
import org.apache.http.HttpStatus;
import org.apache.http.ParseException;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;

import javax.annotation.PostConstruct;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.spec.X509EncodedKeySpec;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * @author sunzhuang
 * @date 2024/6/17 14:47
 * @slogan 天下风云出我辈, 一入代码岁月催
 * @desc todo
 */
@Slf4j
public class AliOssProcessor {

    private static final String CALLBACK_BODY_FORMAT = "{\"bucket\":${bucket},\"filename\":${object},\"size\":${size},\"etag\":${etag},\"mimeType\":${mimeType},\"height\":${imageInfo.height},\"width\":${imageInfo.width},\"userid\":${x:userid},\"ext\":${x:ext}}";

    private static final String PUBLIC_KEY_BEGIN = "-----BEGIN PUBLIC KEY-----";

    private static final String PUBLIC_KEY_END = "-----END PUBLIC KEY-----";

    private static final String OSS_EXCEPTION_NO_SUCH_KEY = "NoSuchKey";

    private static final LoadingCache<String, String> CERT_CACHE = Caffeine
            .newBuilder()
            .maximumSize(10)
            .expireAfterWrite(10, TimeUnit.MINUTES)
            .build(AliOssProcessor::httpGetPubkey);

    private final AliyunOssConfig ossConfig;

    private final DefaultCredentialProvider credsProvider;

    public AliOssProcessor(AliyunOssConfig ossConfig) {
        this.ossConfig = ossConfig;
        this.credsProvider = new DefaultCredentialProvider(ossConfig.getAccessKey(), ossConfig.getAccessSecret());
    }

    private static String httpGetPubkey(String url) {
        try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
            RequestConfig requestConfig = RequestConfig.custom()
                    .setConnectionRequestTimeout(3000)
                    .setSocketTimeout(3000)
                    .setConnectTimeout(1000)
                    .build();
            HttpGet request = new HttpGet(url);
            request.setConfig(requestConfig);
            try (CloseableHttpResponse response = httpClient.execute(request)) {
                if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
                    HttpEntity entity = response.getEntity();
                    if (entity != null) {
                        String pubKey = EntityUtils.toString(entity);
                        if (pubKey == null || pubKey.trim().length() == 0 || !pubKey.contains(PUBLIC_KEY_BEGIN)) {
                            return null;
                        }

                        return pubKey.replace(PUBLIC_KEY_BEGIN, "")
                                .replace(PUBLIC_KEY_END, "")
                                .replace("\r", "")
                                .replace("\n", "");
                    }
                }
                log.error("ossProcessor: callbackCheck isValidCallback httpGet fail");
            }
        } catch (ParseException | IOException e) {
            log.error("ossProcessor: callbackCheck isValidCallback httpGet error", e);
        }
        return null;
    }

    @PostConstruct
    public void init() {
        log.info("ossProcessor: init");
    }

    public OSSClient getOssClient() {
        ClientConfiguration clientConfiguration = new ClientConfiguration();
        if (ossConfig.isUseHttps()) {
            clientConfiguration.setProtocol(Protocol.HTTPS);
        }
        return new OSSClient(ossConfig.getInternalEndpoint(), credsProvider, clientConfiguration);
    }

    public byte[] getObject(OssBucket bucket, String path) {
        return getObject(new GetObjectRequest(bucket.getBucketName(), path));
    }

    public byte[] getObject(GetObjectRequest request) {
        OSSClient ossClient = getOssClient();
        try {
            InputStream objectContent = ossClient.getObject(request).getObjectContent();
            return IOUtils.readStreamAsByteArray(objectContent);
        } catch (OSSException | IOException e) {
            log.error("ossProcessor: getObject error, key={}", request.getKey(), e);
        } finally {
            if (ossClient != null) {
                ossClient.shutdown();
            }
        }
        return null;
    }

    /** 获取上传签名凭证（用于前端直传） */
    public Map<String, Object> getPostObjectPolicy(OssBucket bucket, String path, int expireSeconds, String callbackUrl, String callbackHost, long userId, String ext) {
        OSSClient ossClient = getOssClient();
        try {
            PolicyConditions policyConds = new PolicyConditions();
            policyConds.addConditionItem(PolicyConditions.COND_CONTENT_LENGTH_RANGE, 0, 1048576000);
            policyConds.addConditionItem(MatchMode.StartWith, PolicyConditions.COND_KEY, path.substring(0, path.lastIndexOf('/') + 1));

            LocalDateTime localDateTime = LocalDateTime.now().plusSeconds(expireSeconds);
            Date expireTime = Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant());

            String postPolicy = ossClient.generatePostPolicy(expireTime, policyConds);
            byte[] binaryData = postPolicy.getBytes(StandardCharsets.UTF_8);
            String encodedPolicy = BaseEncoding.base64().encode(binaryData);
            String postSignature = ossClient.calculatePostSignature(postPolicy);

            Map<String, String> map = new HashMap<>();
            map.put("callbackUrl", callbackUrl);
            map.put("callbackHost", callbackHost);
            map.put("callbackBody", CALLBACK_BODY_FORMAT);
            map.put("callbackBodyType", "application/json");

            String json = JSON.toJSONString(map);
            String callbackBase64 = BaseEncoding.base64().encode(json.getBytes(StandardCharsets.UTF_8));

            Map<String, Object> respMap = new HashMap<>();
            respMap.put("accessid", ossConfig.getAccessKey());
            respMap.put("policy", encodedPolicy);
            respMap.put("signature", postSignature);
            respMap.put("key", path);
            respMap.put("host", "https://" + bucket + "." + ossConfig.getEndpoint());
            respMap.put("expire", expireTime.getTime() / 1000);
            respMap.put("callback", callbackBase64);
            respMap.put("x:userid", userId);
            respMap.put("x:ext", ext);
            return respMap;
        } catch (OSSException e) {
            log.error("ossProcessor: getPostObjectPolicy error[{}], path:{}", e.getErrorCode(), path, e);
        } finally {
            if (ossClient != null) {
                ossClient.shutdown();
            }
        }
        return new HashMap<>();
    }

    /** 上传文件 */
    public void putObject(OssBucket bucket, String path, byte[] bytes) throws OSSException, ClientException, IOException {
        OSSClient ossClient = getOssClient();
        try (ByteArrayInputStream bis = new ByteArrayInputStream(bytes)) {
            ossClient.putObject(bucket.getBucketName(), path, bis);
        } finally {
            if (ossClient != null) {
                ossClient.shutdown();
            }
        }
    }

    /** 验证回调是否合法 */
    public boolean isValidCallback(OssCallback cb) {
        byte[] authorization = BaseEncoding.base64().decode(cb.getAuthorization());
        byte[] pubKey = BaseEncoding.base64().decode(cb.getXOssPubKeyUrl());
        String pubKeyAddr = new String(pubKey);
        if (!pubKeyAddr.startsWith("http://gosspublic.alicdn.com/") && !pubKeyAddr.startsWith("https://gosspublic.alicdn.com/")) {
            log.error("ossProcessor: OSS回调签名来源非法地址");
            return false;
        }

        String pubKeyStr = CERT_CACHE.get(pubKeyAddr);
        if (pubKeyStr == null) {
            log.error("ossProcessor: OSS回调获取Pubkey失败, url={}", pubKeyAddr);
            return false;
        }

        try {
            String authStr = java.net.URLDecoder.decode(cb.getRequestPath(), "UTF-8");
            String queryString = cb.getQueryString();
            if (queryString != null && queryString.trim().length() > 0) {
                authStr += "?" + queryString;
            }
            authStr += "\n" + cb.getCallbackBody();

            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
            byte[] encodedKey = BaseEncoding.base64().decode(pubKeyStr);
            PublicKey publicKey = keyFactory.generatePublic(new X509EncodedKeySpec(encodedKey));
            java.security.Signature signature = java.security.Signature.getInstance("MD5withRSA");
            signature.initVerify(publicKey);
            signature.update(authStr.getBytes());
            return signature.verify(authorization);
        } catch (Exception e) {
            log.error("ossProcessor: isValidCallback error", e);
        }
        return false;
    }

    /**
     * 上传本地文件到阿里云
     */
    public void putObject(OssBucket bucket, String path, InputStream inputStream, ObjectMetadata metadata) {
        OSSClient ossClient = getOssClient();
        try {
            ossClient.putObject(bucket.getBucketName(), path, inputStream, metadata);
        } catch (Exception e) {
            log.error("ossProcessor: putObject error, bucket={}, path={}", bucket.getBucketName(), path, e);
        } finally {
            if (ossClient != null) {
                ossClient.shutdown();
            }
        }
    }

    /**
     * 与oss文件的md5对比是否已存在相同文件内容的文件，防止重复上传
     * 该文件之前必须是通过putObject上传的
     * Object生成时会创建ETag (entity tag)，ETag用于标识一个Object的内容。
     * 对于PutObject请求创建的Object，ETag值是其内容的MD5值；对于其他方式创建的Object，ETag值是其内容的UUID。ETag值可以用于检查Object内容是否发生变化。
     * 注意：不是通过文件名字判断文件是否存在
     * https://help.aliyun.com/document_detail/31985.html
     *
     * @param bucket      文件的存放bucket
     * @param path        文件名
     * @param objectBytes 要对比的本地文件
     * @return
     */
    public boolean existObjectJudgeByMd5(OssBucket bucket, String path, byte[] objectBytes) {
        OSSClient ossClient = getOssClient();
        try {
            SimplifiedObjectMeta simplifiedObjectMeta = ossClient.getSimplifiedObjectMeta(bucket.getBucketName(), path);
            String md5 = Hashing.md5().hashBytes(objectBytes).toString();
            return simplifiedObjectMeta.getETag().equalsIgnoreCase(md5);
        } catch (Exception e) {
            boolean noSuchKeyException = e instanceof OSSException && OSS_EXCEPTION_NO_SUCH_KEY.equals(((OSSException) e).getErrorCode());
            // 没有该文件的异常就不要打异常信息了
            if (!noSuchKeyException) {
                log.error("ossProcessor: getSimplifiedObjectMeta error, key={}", path, e);
            }
        } finally {
            if (ossClient != null) {
                ossClient.shutdown();
            }
        }
        return false;
    }

}
